/*
 * Copyright 2008 M-Way Solutions
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.mwaysolutions.ocm.impl;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Calendar;
import java.util.Date;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.mwaysolutions.ocm.OcmException;

/**
 *
 * @author <a href="mailto:m.zdila@mwaysolutions.com">Martin Ždila</a>
 */
public class OcmPropertyUtils {

	private static final Log log = LogFactory.getLog(OcmPropertyUtils.class);


	static boolean isSetter(final Method method) {
		final String methodName = method.getName();
		final boolean returnsVoid = method.getReturnType().equals(void.class);
		final int argLen = method.getParameterTypes().length;

		if (methodName.startsWith("set") && returnsVoid && argLen == 1) {
			return true;
		} else if ((methodName.startsWith("get") || methodName.startsWith("is")) && !returnsVoid && argLen == 0) {
			return false;
		} else {
			throw new IllegalArgumentException(method.toString() + " is not getter nor setter");
		}
	}


	static String getPropertyName(final String getterOrSetterName) {
		if (getterOrSetterName.startsWith("get")) {
			return Character.toLowerCase(getterOrSetterName.charAt(3)) + getterOrSetterName.substring(4);
		} else if (getterOrSetterName.startsWith("is")) {
			return Character.toLowerCase(getterOrSetterName.charAt(2)) + getterOrSetterName.substring(3);
		} else if (getterOrSetterName.startsWith("set")) {
			return Character.toLowerCase(getterOrSetterName.charAt(3)) + getterOrSetterName.substring(4);
		} else {
			throw new IllegalArgumentException(getterOrSetterName + " is not valid name of getter or setter");
		}
	}


	static void setProperty(final Object object, final String name, final Object value) {
		final String methodName = "set" + StringUtils.capitalize(name);
		found: {
			methodLoop: for (final Method method : object.getClass().getMethods()) {
				if (!method.getName().equals(methodName)) {
					continue;
				}

				final Class<?>[] parameterTypes = method.getParameterTypes();
				if (parameterTypes.length != 1) {
					continue;
				}

				try {
					if (value == null) {
						method.invoke(object, value);
						break found;
					}

					final Class<?> valueClass = value.getClass();
					final Class<?> parameterType = parameterTypes[0];

					if (parameterType.isAssignableFrom(valueClass) ||
							valueClass == Boolean.class && parameterType == boolean.class) {
						method.invoke(object, value);
						break found;
					} else if (valueClass == Double.class) {
						if (parameterType.isAssignableFrom(Float.class) || parameterType == float.class) {
							method.invoke(object, ((Number) value).floatValue());
							break found;
						} else if (/*parameterType.isAssignableFrom(Double.class) || */parameterType == double.class) {
							method.invoke(object, ((Number) value).doubleValue());
							break found;
						}
					} else if (valueClass == Long.class) {
						if (parameterType.isAssignableFrom(Short.class) || parameterType == short.class) {
							method.invoke(object, ((Number) value).shortValue());
							break found;
						} else if (parameterType.isAssignableFrom(Integer.class) || parameterType == int.class) {
							method.invoke(object, ((Number) value).intValue());
							break found;
						} else if (/*parameterType.isAssignableFrom(Long.class) || */parameterType == long.class) {
							method.invoke(object, ((Number) value).longValue());
							break found;
						} else if (parameterType.isAssignableFrom(Byte.class) || parameterType == byte.class) {
							method.invoke(object, ((Number) value).byteValue());
							break found;
						}
					} else if (valueClass == boolean[].class && parameterType.isAssignableFrom(Boolean[].class)) {
						final Object[] parameter = new Boolean[Array.getLength(value)];
						for (int i = 0; i < parameter.length; i++) {
							parameter[i] = Array.get(value, i);
						}
						method.invoke(object, (Object) parameter);
						break found;
					} else if (valueClass == double[].class) {
						if (parameterType == float[].class) {
							final float[] parameter = new float[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Double) Array.get(value, i)).floatValue();
							}
							method.invoke(object, parameter);
							break found;
						} else if (parameterType == Float[].class) {
							final Float[] parameter = new Float[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Double) Array.get(value, i)).floatValue();
							}
							method.invoke(object, (Object) parameter);
							break found;
						/*} else if (parameterType == double[].class) {*/
						} else if (parameterType == Double[].class) {
							final Double[] parameter = new Double[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Double) Array.get(value, i)).doubleValue();
							}
							method.invoke(object, (Object) parameter);
							break found;
						}
					} else if (valueClass == long[].class) {
						if (parameterType == byte[].class) {
							final byte[] parameter = new byte[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Long) Array.get(value, i)).byteValue();
							}
							method.invoke(object, parameter);
							break found;
						} else if (parameterType == Byte[].class) {
							final Byte[] parameter = new Byte[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Long) Array.get(value, i)).byteValue();
							}
							method.invoke(object, (Object) parameter);
							break found;
						} else if (parameterType == short[].class) {
							final short[] parameter = new short[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Long) Array.get(value, i)).shortValue();
							}
							method.invoke(object, parameter);
							break found;
						} else if (parameterType == Short[].class) {
							final Short[] parameter = new Short[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Long) Array.get(value, i)).shortValue();
							}
							method.invoke(object, (Object) parameter);
							break found;
						} else if (parameterType == int[].class) {
							final int[] parameter = new int[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Long) Array.get(value, i)).intValue();
							}
							method.invoke(object, parameter);
							break found;
						} else if (parameterType == Integer[].class) {
							final Integer[] parameter = new Integer[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Long) Array.get(value, i)).intValue();
							}
							method.invoke(object, (Object) parameter);
							break found;
						/*} else if (parameterType == long[].class) {*/
						} else if (parameterType == Long[].class) {
							final Long[] parameter = new Long[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Long) Array.get(value, i)).longValue();
							}
							method.invoke(object, (Object) parameter);
							break found;
						}
					} else if (valueClass.isArray() && parameterType.isArray()) { // try to convert Object[] to specific array
						final int len = Array.getLength(value);
						final Object parameter = Array.newInstance(parameterType.getComponentType(), len);
						for (int i = 0; i < len; i++) {
							try {
								Array.set(parameter, i, Array.get(value, i));
							} catch (final IllegalArgumentException e) {
								continue methodLoop;
							}
						}
						method.invoke(object, parameter);
						break found;
					} else if (InputStream.class.isAssignableFrom(valueClass) && parameterType == byte[].class) {
						try {
							method.invoke(object, IOUtils.toByteArray(((InputStream) value)));
						} catch (final IOException e) {
							throw new OcmException(e);
						}
						break found;
					} else if (Calendar.class.isAssignableFrom(valueClass) && parameterType.isAssignableFrom(Date.class)) {
						method.invoke(object, ((Calendar) value).getTime());
						break found;
					} else if (valueClass == String.class && Enum.class.isAssignableFrom(parameterType)) {
						method.invoke(object, createEnum(parameterType, (String) value));
						break found;
					}
				} catch (final IllegalArgumentException e) {
					throw new OcmException(e);
				} catch (final IllegalAccessException e) {
					throw new OcmException(e);
				} catch (final InvocationTargetException e) {
					throw new OcmException(e);
				}
			}

			log.warn("could not find appropriate setter method for property " + name + " and value " +
					value.getClass().getCanonicalName() + " in " + object.getClass().getCanonicalName() + " class");

		} // found
	}


	@SuppressWarnings("unchecked")
	private static Enum createEnum(final Class parameterType, final String value) {
		return Enum.valueOf(parameterType, value);
	}


	static Object getProperty(final Object object, final String name) {
		try {
			return PropertyUtils.getSimpleProperty(object, name);
		} catch (final IllegalAccessException e) {
			throw new OcmException(e);
		} catch (final InvocationTargetException e) {
			throw new OcmException(e);
		} catch (final NoSuchMethodException e) {
			throw new OcmException(e);
		}
	}

}
